4.2.1 编码方法及其声音实现

  1. 编码方法及其声音实现
    1. 脉冲编码
    2. 幅度编码
    3. 时间编码
    4. 频率编码
    5. 相位编码
    6. BPSK & QPSK 编码
    7. OFDM编码
    8. 载波调制

编码方法及其声音实现

在本章节中,我们将介绍几种常用的编码方法,并将这几种编码解码方法实现到声音通信上,实现对声音信号的解码。

脉冲编码

脉冲编码是利用相邻两个脉冲信号之间的时间间隔来编码数据。可以指定长度的间隔代表特定的二进制串。最简单的脉冲编码是使用一长一短两种间隔,分别代表“0”和“1”。在解码时只需要识别出每个脉冲信号的起始位置,就可以得到不同脉冲之间的间隔,从而可以根据每个间隔的长短将其解码为“0”或“1”。

脉冲编码原理

脉冲编码由于使用简单的对应规则将“0”和“1”编码为不同长度的间隔,在解码时可以简单的根据信号幅度得到每个脉冲的起始位置来获得间隔的长短,这种编解码方法的优点是计算开销非常小。

脉冲编码的缺点是编码效率低,每个脉冲之间需要有足够的距离来防止多径效应造成的回声影响到下一个脉冲的判断。想要提高脉冲编码的数据速率,可以从两个方面来考虑。第一个方面是可以缩短编码长度,不仅可以在保证解码正确率的条件下尽可能缩短脉冲之间用于编码数据的时间间隔,还可以通过缩短脉冲信号的持续时间来使得同样时间下传输更多的数据。

缩短脉冲间隔可以提高编码效率

第二个方面,可以通过设置多种编码长度,使一个编码位携带更多的信息。如下图中所示,设置2种编码长度时,每个编码位携带1bit信息;设置4中编码长度时,每个编码位携带2bit信息;设置8种编码长度时,每个编码位携带3bit信息。

设置多种编码长度

当然,我们不能无限的通过增加不同的编码长度来提高编码效率。当不同的编码长度之间的区分度越小,解码时将他们区分开的难度也越大。

另外,如果不同的编码单位出现的概率不同,我们还可以使用霍夫曼编码的思想来进一步优化脉冲编码的速率。比如说在一个待发送的文件中“0”的个数大于“1”的个数,使用较短的间隔代表“0”,长的间隔代表“1”,可以使总的发送时间更短。

实现到声音通信上:
信号:使用18kHz的声音信号作为脉冲信号,采样频率设置为48kHz。17kHz以上频率的声音信号是大多数成年人都听不到的,选用18kHz可以使人不受干扰。
编码设计:脉冲长度设置为100个采样点,由于采样频率为48kHz,脉冲的持续时间是100/48000≈2.1ms。脉冲之间空白的间隔采样点数与编码的对应规则见下表:
脉冲间隔(sample)| 编码
— | —
50 | 00
100 | 01
150 | 10
200 | 11

进行声音通信首先是要产生声音信号,在脉冲编码中,我们需要生成固定频率(18KHz)和固定长度(100个采样点)的脉冲信号:

%% impulse coding
fs = 48000;%设置采样频率
f = 18000;%指定声音信号频率
time = 0.0025;%指定生成的信号持续时间
t = 0:1/fs:time;%设置每个采样点数据对应的时间
t = t(1:100);%截取出100个时间上的采样点
impulse = sin(2*pi*f*t);%生成频率为f的正弦信号

接下来我们生成用于编码的空白部分:

delta = 50;
pause0 = zeros(1,delta);%编码00
pause1 = zeros(1,2*delta); %编码01
pause2 = zeros(1,3*delta); %编码10
pause3 = zeros(1,4*delta); %编码11

设置待传输的字符串:

str = 'Tsinghua University';

为了通过信号把字符串传输出去,我们需要将其转为二进制串。实现名为“string2bin”的函数,把字符数组转化为0,1串:

function [ binary ] = string2bin( str )
%   把字符串转换成二进制串
ascii = abs(str);
L = length(ascii);
binary = zeros(L,8);
for i=1:L
    binary_str = dec2bin(ascii(i));
    binary_str_index = length(binary_str);
    for j = 8:-1:1
        if binary_str_index >0
            binary(i,j) = str2num(binary_str(binary_str_index));
        else
            binary(i,j) = 0;
        end
        binary_str_index = binary_str_index-1;
    end
end
binary = reshape(binary',[L*8,1]);
end

调用string2bin函数时传入之前设置的待传输字符串,就可以得到等待传输的二进制串,将其存储在变量“message”中:

message = string2bin( str )';%调用函数把字符串转为二进制串
%因为我们设计的编码是每个码元代表2个bit,这里要把二进制串转为4进制串
[~,m_Length] = size(message);
message4 = [];
for i = 1:m_Length/2
    % 把二进制串中的每两位进行结合,得到四进制串
    message4 = [message4,message(i*2-1)*2+message(i*2)];
end

生成编码数据:

output = [];
% 根据四进制串中的值,将impulse和对应的空白信号添加到输出信号中
for i = 1:m_Length/2
    if message4(i)==0
        output = [output,impulse,pause0];
    elseif message4(i)==1
        output = [output,impulse,pause1];
    elseif message4(i)==2
        output = [output,impulse,pause2];
    else
        output = [output,impulse,pause3];
    end  
end
% 在输出信号前加一段空白,避免播放器在信号刚开始的位置出现失真的情况。
output = [pause3,output,impulse];
% 在figure中画出输出的时域信号
figure(1);
plot(output);
axis([-500 17500 -3 3]);
% 将输出信号写入到音频文件中,需要指明文件名、数据、和采样频率。
audiowrite('message.wav',data,fs);

最终生成的输出信号时域信号如图所示,横轴代表采样点的序号,纵轴代表幅度:

编码生成的时域信号

得到编码过信息的声音文件后,我们将文件存储在一个android 手机上,并使用一个声音播放器打开此文件进行播放。同时,使用我们在前面章节中开发的录音应用把携带有信息的声音信号录制下来存储到录音文件中。
用matlab读取录音文件“r.wav”,并将读出的数据在图中展示出来:

%% 读取录音文件中的数据
[data, fs] = audioread('r.wav');
figure(1);
plot(data);
hold on;

展示的结果如下图所示:

接收到的声音信号

因为我们这里展示的是简单的编码方式,单个声音信道录到的数据就足够解码,我在录音时设置了单声道的录制。相应的这里得到的声音信号显示是只有一路信号。
在进行具体的解码之前,我们先来看一下impulse解码的原理。由于impulse coding 使用的是impulse之间的间隔来编码数据,所以解码的时候关键在于得到每两个相邻脉冲之间的间隔,从而将其转换为对应的二进制数据。要得到脉冲之间的间隔就需要首先得到每个脉冲的起始和结束时间。又因为每个脉冲的长度是固定的,所以我们只需要知道脉冲的起始位置即可。
借助傅里叶变换,我们可以轻易知道一段时域信号中某个频率信号的强度。通过把时域信号进行分段的傅里叶变换,每段信号中18kHz信号的强度都可以被计算出来。由于我们设计的脉冲长度是100个采样点,当我们把傅里叶变换的窗口长度设置为100时,只有窗口从脉冲起始点开始截取信号的时候才能使得整个时域窗口中都充满18kHz的声音信号。若是窗口起始点不在脉冲起始的位置,那么时域窗口中将不可避免的包含到一部分空白信号(没有声音信号,采样值接近0)。根据傅里叶变换的原理,频域上的能量强度是时域上对应频率能量的叠加。所以当窗口对齐脉冲起始位置时,傅里叶变换得到的18kHz处的能量是最高的。我们可以记录下每个时域窗口对应的18kHz的能量强度,通过寻找极大值得到每个脉冲信号的起始位置。

脉冲信号和时域窗口

接下来我们进行解码操作。首先对信号进行滤波,去除掉环境中的噪音只保留信号编码所用到的18kHz的声音信号:

%% 对录音数据进行滤波
%定义一个带通滤波器
hd = design(fdesign.bandpass('N,F3dB1,F3dB2',6,17500,18500,fs),'butter');
%用定义好的带通滤波器对data进行滤波
data = filter(hd,data);

(思考:滤波的操作实际上在脉冲编码中并不是必要的,你知道是为什么吗?)
接下来我们对录音信号进行滑动窗口的傅里叶变换,得到每一段数据中18kHz信号的强度信息:

%% 对数据进行带滑动窗口的傅里叶变换。得到每一段数据中18kHz信号的强度信息
f = 18000;%目标频率为18kHz
[n,~] = size(data);%获取数据的长度值
window = 100;%设置窗口大小为100个采样点
impulse_fft = zeros(n,1);%定义变量数组impulse_fft,用于存储每个时刻对应的数据段中18kHz信号的强度
for i= 1:1:n-window
    %对从当前点开始的window长度的数据进行傅里叶变换
    y = fft(data(i:i+window-1));
    y = abs(y);
    %得到目标频率傅里叶变换结果中对应的index
    index_impulse = round(f/fs*window);
    %考虑到声音通信过程中的频率偏移,我们取以目标频率为中心的5个频率采样点中最大的一个来代表目标频率的强度
    impulse_fft(i)=max(y(index_impulse-2:index_impulse+2));
end
% 在figure中展示每个窗口对应的18kHz信号的强度
figure(2);
plot(impulse_fft);

在图中展示每个时域窗口的信号对应的18kHz信号的强度,如下图所示:

每个时域窗口对应的18kHz信号的强度

对局部进行放大,我们可以观察到锯齿形的曲线:

时域上的18kHz信号强度的局部放大

我们的目的是通过找极大值准确的得到每个脉冲信号的起始位置,然而锯齿形的信号的最大值可能不严格出现在信号峰的中间位置。我们需要通过滑动窗口平均来对impulse_fft进行均值滤波,得到一个平滑的曲线。在这里我们设置一个大小为11的窗口:

% 滑动平均(均值滤波)
sliding_window = 5;
for i= 1+sliding_window:1:n-sliding_window
    impulse_fft(i)=mean(impulse_fft(i-sliding_window:i+sliding_window));
end
在figure中展示平滑后的impulse_fft
figure(2);
plot(impulse_fft);
hold on;

我们可以从下图中看出,均值滤波有效地把锯齿形的信号转化成了相对平滑的信号。对于滑动窗口的大小,可以根据实际需要进行调整。

经过滑动平均的18kHz信号强度的局部放大

由于在实际操作中我们不能保证经过了平滑之后的信号在峰的两侧都是单调的,所以我们用局部最大值来替代极大值的判断。通过找到局部的最大值得到峰的中间位置,从而得到脉冲信号的起始位置。由于脉冲的长度为100,所以我们再次使用一个长度为100的窗口。这次的窗口是以当前点为窗口的中间,往前后各取半个窗口的长度。当中心点的值是整个窗口中的最大值时,说明左右两侧的点都比中间点的值小,也就是说,当前窗口的中心点是一个峰。为了去除空白数据处的曲线波动对峰值判断的干扰,我们多加了一个对峰的数值的判断,当数据值小于等于0.3时,无论曲线在此处的走势如何都不认为这是一个峰。

% 取出impulse 起始位置(峰的中间位置)
position_impulse=[];%用于存储峰值的index
half_window = 50;
for i= half_window+1:1:n-half_window
    %进行峰值判断
    if impulse_fft(i)>0.3 && impulse_fft(i)==max(impulse_fft(i-half_window:i+half_window))
        position_impulse=[position_impulse,i];
    end
end

根据我们前面的分析可知,峰值的位置就是脉冲的起始位置。为了验证这个结论,我们把得到的峰值位置在时域信号图中展示出来,并计算相邻两个脉冲之间的间隔:

%% 在图中表示出脉冲起始位置并计算相邻两个脉冲之间的间隔
[~,N]= size(position_impulse);
%定义变量delta_impulse用于存储相邻两个脉冲之间的间隔
delta_impulse=zeros(1,N-1);
for i = 1:N-1
    %在18kHz信号的强度图中标出脉冲起始位置
    figure(2);
    plot([position_impulse(i),position_impulse(i)],[0,0.8],'m');
    %在时域信号上标出脉冲起始位置
    figure(1);
    plot([position_impulse(i),position_impulse(i)],[0,0.2],'m','linewidth',2);
    %计算两个相邻脉冲之间的间隔。-100是减去脉冲信号长度
    delta_impulse(i) = position_impulse(i+1) -  position_impulse(i) -100;
end

脉冲起始位置在原始时域声音信号上的展示如下图所示。观察发现代表着我们计算得到的脉冲起始位置的洋红色线条所切割的位置并不是真正的脉冲信号起始位置。在洋红色线条之前已经有一段的声音信号存在了。

计算得到的脉冲起始在真实时域信号中与脉冲起始不匹配

然而这些洋红色线条在18kHz强度的时域图中与峰值很好的一一对应,如下图所示。也就是说,18kHz信号强度的时域图中的峰值并不是出现在脉冲的起始位置。

计算得到的脉冲起始位置和18kHz信号强度峰值整齐对应

这个结论我们最初的理论分析是不一致的。为了解释这一现象,我们将真实时域信号和分窗口傅里叶变换得到的18kHz信号强度时域图画在下图中进行观察。我们发现

18kHz信号强度峰值并没有出现在信号起始处

这个现象其实是滤波造成的。如果我们把上图中的原始信号替换成滤波之后的信号,就会发现滤波后的每个脉冲的开头和结尾处的信号比中间的弱。这个滤波器的特性所导致的现象,再加上多径效应造成的回声拖尾现象,使得滑动窗口傅里叶变换得到的最大值并不是出现在脉冲的起始位置。

18kHz信号强度时域图以及滤波之后的声音信号

若我们不对信号进行滤波,而是直接用原始的声音数据进行滑动窗口傅里叶变换,得到的结果如下图所示。我们可以看到18kHz信号强度的峰值确实出现在了声音信号的起始位置,但由于没有进行滤波,原始信号中所存在的频率更杂乱,使得我们得到的18kHz信号强度曲线也更加波折。

18kHz信号强度峰值并没有出现在信号起始处

尽管用滤波前的原始数据还是滤波后的数据进行滑动窗口傅里叶变换得到的峰值位置不一致,事实上,这两种方式都可以成功解码出数据。这是因为我们解码数据依靠的是相邻两个脉冲信号之间的间隔。就算识别出来的脉冲信号的绝对位置有偏差,只要每个脉冲信号位置都偏差大致相似的采样点数,相邻两个脉冲信号之间的间隔是大致不变的。当我们设计编码时使代表不同码元的间隔之间差异足够大,就可以保证解码的准确性。
接下来我们使用相邻脉冲之间的间隔进行解码。根据我们设计编码时定义的对应规则把不同长度的间隔映射为不同的编码数据。另外,由于噪声等的影响,我们得到的间隔长度不会严格等于设计值。这时就需要我们在解码时加入一定的鲁棒性。在这个实验中,我们认为只要实际间隔值和设计值之间的误差小于10,就解码出对应的数据,否则解码失败。误差的阈值可以根据信道、信号等的实际情况进行设置。

%% 解码
%由于每个码元对应2bit,所以先把间隔对应到4进制数
decode_message4 = zeros(1,N-1)-1;
for i = 1:N-1
    if delta_impulse(i) - 50 >-10 &&delta_impulse(i) - 50 <10
        decode_message4(i) = 0;
    elseif delta_impulse(i) - 100 >-10 &&delta_impulse(i) - 100 <10
        decode_message4(i) = 1;
    elseif delta_impulse(i) - 150 >-10 &&delta_impulse(i) - 150 <10
        decode_message4(i) = 2;
    elseif delta_impulse(i) - 200 >-10 &&delta_impulse(i) - 200 <10
        decode_message4(i) = 3;
    end
end
% 把四进制转化为二进制
decode_message = zeros(1,(N-1)*2)-1;
for i = 1:N-1
    if decode_message4(i) == 0
        decode_message(i*2-1)=0;
        decode_message(i*2)=0;
    elseif decode_message4(i) == 1
        decode_message(i*2-1)=0;
        decode_message(i*2)=1;
    elseif decode_message4(i) == 2
        decode_message(i*2-1)=1;
        decode_message(i*2)=0;
    elseif decode_message4(i) == 3
        decode_message(i*2-1)=1;
        decode_message(i*2)=1;
    end
end

现在我们解码出了声音信号中编码的二进制串,要将其转化为可解读的信息还需要将二进制串变成字符串。我们实现一个与string2bin函数对应的bin2string函数来实现这个功能:

function [ str ] = bin2string( binary )
%UNTITLED2 此处显示有关此函数的摘要
%   把二进制串转化为字符串
L = length(binary);
str = [];
binary = reshape(binary',[8,L/8]);
binary = binary';
for i=1:L/8
    s= 0;
    for j = 1:8
        s = s+2^(8-j)*binary(i,j);
    end
    str = [str,char(s)];
end
end

最后,调用bin2string函数:

%把二进制数据根据ascii码值解出对应的字符串
str = bin2string(decode_message)

得到可以解读的数据串:

18kHz信号强度峰值并没有出现在信号起始处

幅度编码

幅度编码是利用为信号设置不同的幅度来实现编码的目的。最简单的幅度编码是ON-OFF Keying (OOK)。OOK使用固定频率的信号代表“1”,没有信号代表“0”,两种码元的长度相同。下面是使用OOK编码的示意图:

幅度编码

幅度编码的效率提高可以通过设置不同的振幅级别来实现,使得每个码元所携带的信息更多:

增加振幅级别可以提高编码效率

在幅度编码中,首先需要考虑的问题是要使的不同码元对应的幅度有足够大的区分度,否则解码时无法区分不同的幅度。另外一点需要考虑的就是信道的特性。信号所传输的信道增益是否是随时间稳定的?如果信道增益发生变化,那么就很可能使得本来幅度较高的信号在经过信道传输之后比原本幅度低的信号还弱。这就容易导致解码错误。声音信道就是一种增益比较容易变化的信道,周围反射环境的变化会使得信道增益出现明显变化。这种情况下可以通过缩短数据包的长度来折中。由于信道增益的变化依赖于多径环境的变化,我们知道多径环境不会发生突变,只会发生连续的变化。只要数据包的长度足够短,那么就可以保证在这个数据包发送的过程中,声音信道的增益的变化是比较小的。
(思考题:信号经过信道传输之后,会发生强度的衰减。当接收端接收到信号时,信号幅度的绝对值已经发生了很大的改变。如何才能保证正确解码出信号中的数据呢?)
//(比如:每个数据包起始处先按顺序把每种码元发一遍,这样接收者就知道每种码元收到时的强度了)

时间编码

??这个到底叫啥。。
利用一个单位编码内信号出现的时间进行数据的编码的方式。

频率编码

频率编码也就是Frequency shift keying (FSK),使用的是不同的频率或不同的频率组合来编码不同的数据。
最简单的频率编码也可以用ON-OFF Keying (OOK)来表示,使用固定频率$f\not=0$的信号代表“1”,用$f=0$的信号代表“0”。
可以设置多种不同的频率$F={f_0,f_1,f_2,….f_n}$来代表不同的数据,但需要使不同频率之间的额差异足够大。否则由于离散傅里叶变换的精度问题,相近的频率会很难区分。
使用不同的频率组合进行编码可以大大提高编码的效率。当使用6种不同的频率进行单一频率编码时,最多只能产生7个码元:每个频率是一个码元,空白信号代表一个码元。同样是6中不同的频率,若是使用两种频率的组合进行编码,就可以产生$C_6^2=15+1 = 16$种码元(一个空白信号码元)。

相位编码

BPSK & QPSK 编码

OFDM编码

载波调制

上变频 下变频


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。

文章标题:4.2.1 编码方法及其声音实现

文章字数:5.3k

本文作者:WiSys Lab

发布时间:2019-07-09, 17:00:00

最后更新:2019-07-23, 08:43:56

原始链接:http://yoursite.com/2019/07/09/4.2 编码方法及其声音实现/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录